Skip to content

refactor: use libssz instead of ethereum_ssz#242

Open
MegaRedHand wants to merge 13 commits intomainfrom
ssz-lib
Open

refactor: use libssz instead of ethereum_ssz#242
MegaRedHand wants to merge 13 commits intomainfrom
ssz-lib

Conversation

@MegaRedHand
Copy link
Copy Markdown
Collaborator

@MegaRedHand MegaRedHand commented Mar 18, 2026

This PR moves to using libssz, our own SSZ library, which is faster and has no_std support.

We still keep ethereum_ssz as a transitive dependency from lean-multisig, but this might change in the future.

@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Review Summary

This PR migrates ethlambda from the legacy ethereum_ssz/tree_hash ecosystem to a new in-house SSZ implementation (libssz). The change is large but necessary to eliminate external dependencies and align with the project's minimalist philosophy.

Correctness & Consensus Safety

  • State transition logic preserved: All hash-tree-root computations, bitfield operations, and serialization paths have been mechanically updated without altering consensus semantics.
  • Genesis & checkpoint sync: H256 construction from hash_tree_root() is now explicit (H256(state.hash_tree_root())), preventing silent type mismatches.

⚠️ Security & Edge Cases

  1. Bitfield capacity checks (crates/blockchain/src/store.rs:969, state_transition/src/justified_slots_ops.rs:55)

    • JustifiedSlots::with_length(required_capacity) can panic on overflow. Add explicit bounds checking before allocation.
  2. Signature parsing (crates/common/types/src/signature.rs:40)

    • SignatureParseError wraps format!("{e:?}"), leaking internal details. Use Display instead of Debug for production logs.
  3. SSZ list truncation (crates/storage/src/store.rs:119)

    • ErrorMessage::try_from(truncated) silently truncates long strings. Assert or log when truncation occurs to avoid silent data loss.

🔍 Performance & Idiomatic Rust

  • Avoid repeated bit counting (crates/blockchain/src/store.rs:505-510)
    Replace the manual byte-wise count with aggregated.proof.participants.count_ones() if SszBitlist exposes it.

  • Redundant clones (crates/common/types/src/genesis.rs:56)
    pubkey.clone() for a 52-byte array is unnecessary; use *pubkey.

  • Iterator allocation (state_transition/src/lib.rs:248-254)
    The collect::<Vec<bool>>() can be avoided by iterating directly over the bitfield range.

📦 Dependency Hygiene

  • Duplicate ethereum_ssz in crypto (crates/common/crypto/Cargo.toml)
    The legacy ethereum_ssz is only needed for Devnet2XmssAggregateSignature. Gate it behind a #[cfg(feature = "legacy-ssz")] to prevent accidental usage elsewhere.

  • Unused imports (crates/net/p2p/src/req_resp/codec.rs:17)
    Remove the unused SszDecode import.

Testing & Maintainability

  • Test data regeneration (crates/net/p2p/src/gossipsub/encoding.rs:59)
    The ignored test references old field order. Regenerate test vectors to match the new SszEncode layout.

  • Bitfield iteration (crates/common/types/src/attestation.rs:71-77)
    The new iterator is correct but less efficient. Consider adding a set_bits() method to SszBitlist for direct iteration.

Action Items

  1. Add bounds checks for bitfield allocations.
  2. Replace Debug with Display in error messages.
  3. Remove legacy ethereum_ssz from non-crypto crates.
  4. Regenerate test fixtures for P2P encoding tests.

Verdict: The migration is sound with minor polish needed. No consensus-breaking issues detected.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. High - test pre-state is no longer faithfully loaded from fixtures
    In crates/common/test-fixtures/src/lib.rs:158 and crates/common/test-fixtures/src/lib.rs:161, justified_slots and justifications_validators are hard-reset to empty instead of being reconstructed from fixture data (TestState still carries both at lines 127-133).
    This can invalidate consensus/state-transition spectests by mutating the initial state, masking real regressions (especially justification/finalization behavior).
    Suggested fix: reconstruct both fields from fixture contents (with finalized-slot-relative mapping for justified_slots if representation changed).

  2. Medium - wire-format interoperability risk is currently unprotected
    BlockSignatures SSZ field order is now attestation_signatures then proposer_signature in crates/common/types/src/block.rs:51, and the cross-client decode test is explicitly ignored in crates/net/p2p/src/gossipsub/encoding.rs:59.
    Given SSZ container ordering is part of the hash/wire format, this is a potential network-compatibility break unless all peers are upgraded in lockstep. At minimum, keep an active compatibility test for both formats during migration.

  3. Low - participant count metric may be inaccurate
    In crates/blockchain/src/store.rs:505, counting participants via participants.as_bytes().count_ones() depends on internal bitlist byte representation. This can drift from logical participant count if trailing/non-logical bits ever appear.
    Safer/clearer: aggregated.proof.participant_indices().count().

Overall, the SSZ migration itself looks mostly consistent (hash root and encode/decode replacements are generally coherent), but Point 1 is a significant correctness gap in consensus test coverage.


Automated review by OpenAI Codex · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough context. Let me write up the review.


PR #242 Review: refactor: use libssz instead of ethereum_ssz

This is a large mechanical migration from ethereum_ssz/ssz_types/tree_hash to the in-house libssz (lambdaclass/libssz). The API renaming is consistent and systematic (EncodeSszEncode, as_ssz_bytes()to_ssz(), TreeHashHashTreeRoot, tree_hash_root()hash_tree_root(), etc.). The H256 type is replaced with a bespoke newtype. Overall the mechanical parts look clean, but there are a few issues worth addressing.


Bug: H256 serializes without 0x prefix

crates/common/types/src/primitives.rs, H256 Serialize impl

impl serde::Serialize for H256 {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&format!("{:x}", self))
    }
}

format!("{:x}", self) invokes LowerHex, which emits hex digits with no prefix (e.g. "aabbcc..."). The old ethereum-types::H256 serialized as "0x...". Meanwhile Display correctly adds "0x", but isn't used here.

The deserializer accepts both forms, so round-tripping works internally. But any external consumer — Ream, Zeam, qlean, Lantern, or standard tooling — receiving JSON from the RPC endpoints will get block roots and state roots without the 0x prefix, breaking interoperability. Checkpoint and BlockHeader both derive Serialize and embed H256, so all JSON API responses are affected.

Fix: use format!("{}", self) (the Display impl) instead of format!("{:x}", self).


Bug: H256::from_slice silently pads/truncates

crates/common/types/src/primitives.rs

pub fn from_slice(bytes: &[u8]) -> Self {
    let mut arr = [0u8; 32];
    let len = bytes.len().min(32);
    arr[..len].copy_from_slice(&bytes[..len]);
    Self(arr)
}

If fewer than 32 bytes are supplied the result is silently zero-padded, producing a different hash from the one intended. This is particularly dangerous for hash/root values — a caller passing a truncated slice would get an invalid (but non-panicking) H256 that silently participates in fork choice or state root comparisons. This function should either assert/panic on wrong-length input or return Result<Self, _>.


Off-by-one in num_participants (logging only, but misleading)

crates/blockchain/src/store.rs

let num_participants: u32 = aggregated
    .proof
    .participants
    .as_bytes()
    .iter()
    .map(|b| b.count_ones())
    .sum();

SSZ bitlists are encoded with a sentinel 1 bit appended after the last data bit, so count_ones() across raw bytes over-counts by 1. For example, a 3-element bitlist [1, 0, 1] encodes as 0b00001101, giving count_ones() = 3 rather than 2.

This is used only for the info! log line, so it has no consensus impact, but it will consistently misreport participant counts in monitoring/debugging. The old num_set_bits() presumably accounted for the sentinel. Consider restoring or using a method from libssz that operates on the logical length.


Missing Deserialize on ValidatorPubkeyBytes

crates/common/types/src/state.rs

ValidatorPubkeyBytes implements Serialize but not Deserialize. The old pub type ValidatorPubkeyBytes = [u8; 52] got blanket impls for both. If any code path deserializes a Validator or ValidatorPubkeyBytes from JSON/YAML (e.g. checkpoint sync response parsing, test fixture loading), it will fail to compile or panic at runtime. Verify that all deserialization sites either go through SSZ (which is covered) or have an explicit deserialization path.


set() panics silently discarded (minor)

crates/blockchain/src/store.rs, crates/blockchain/state_transition/src/justified_slots_ops.rs

The old bits.set(vid, true).expect("...") made it visible when an out-of-bounds set was attempted. The new bits.set(vid as usize, true) has no returned Result, so the behaviour (panic vs. no-op) on an out-of-bounds index depends entirely on how libssz implements set. The logic that computes capacity (max_id + 1) before calling set looks correct, but this is now an unverified invariant. A comment or a debug assertion (debug_assert!(vid < bits.len())) would preserve the earlier intent.


Error formatting style inconsistency (minor)

crates/blockchain/src/key_manager.rs and crates/common/types/src/signature.rs

.map_err(|e| KeyManagerError::SignatureConversionError(format!("{e:?}")))?;

{e:?} (Debug) is used instead of {e} (Display). The existing codebase uses %err (Display) in tracing and consistent Display formatting in error messages. Using Debug adds noise ("..." wrappers, Rust enum path prefixes) to user-facing error strings. Change to format!("{e}").


Positive notes

  • The extend_to_slot rewrite (replacing union with explicit bit copy) is logically correct and the early-exit guard ensures it only extends, never shrinks.
  • The pinning of libssz to a specific git commit (a6ffa33) is good practice for reproducibility.
  • The ValidatorPubkeyBytes newtype with manual SszEncode/SszDecode/HashTreeRoot impls is a clean solution to the 52-byte-array limitation.
  • The SIGNATURE_SIZE: usize = 3112 constant replacing the typenum arithmetic (Diff<U3600, U488>) is much more readable.
  • Retaining ethereum_ssz in ethlambda-crypto with a clear comment explaining why (external Devnet2XmssAggregateSignature uses the old API) is the right pragmatic approach.
  • The shift_window rewrite looks correct.
  • All uses of H256(x.hash_tree_root()) are consistent and mechanical.

Automated review by Claude (Anthropic) · custom prompt

rec_aggregation = { git = "https://github.com/leanEthereum/leanMultisig.git", rev = "e4474138487eeb1ed7c2e1013674fe80ac9f3165" }
# ethereum_ssz is needed for Devnet2XmssAggregateSignature from lean-multisig,
# which implements the old ethereum_ssz Encode/Decode traits
ethereum_ssz = { package = "ethereum_ssz", version = "0.10.0" }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lean-multisig is still using ethereum_ssz::Encode/Decode traits. So we are depending on both libraries

Copy link
Copy Markdown
Collaborator

@pablodeymo pablodeymo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on the migration, clean and well-structured. CI is green and all tests pass. A few observations:

H256(x.hash_tree_root()) wrapping pattern

There are ~25 call sites wrapping hash_tree_root() in H256(...). I checked libssz and confirmed there's no ethereum_types feature flag — HashTreeRoot returns [u8; 32] by design, which makes sense for a generic no_std library.

Since H256 is now a local type, we can add a convenience trait on our side to reduce the boilerplate. Something like:

// In ethlambda_types::primitives
use ssz_merkle::HashTreeRoot;

pub trait HashTreeRootExt: HashTreeRoot {
    fn tree_hash_root(&self) -> H256 {
        H256(self.hash_tree_root())
    }
}
impl<T: HashTreeRoot> HashTreeRootExt for T {}

This would turn H256(block.hash_tree_root()) back into block.tree_hash_root() everywhere. Not a blocker for this PR — could be a quick follow-up.

ethereum_ssz residual in crypto crate

The comment in crates/common/crypto/Cargo.toml explains this well — lean-multisig still uses the old ethereum_ssz traits. Just flagging it for tracking so it gets cleaned up when lean-multisig migrates.

ValidatorPubkeyBytes manual impls

The ~40 lines of manual SszEncode/SszDecode/HashTreeRoot for the 52-byte pubkey newtype are correct but boilerplate. Could libssz support arbitrary [u8; N] via const generics? Would eliminate the need for manual impls on uncommon byte array sizes. Low priority.

Bitlist set() no longer returns Result

The old set(i, true).unwrap() pattern is now just set(i, true). Cleaner, but what happens on out-of-bounds? Silent no-op or panic? Worth documenting the contract.

Overall this is a solid migration. The const generics over typenum are a clear readability win, and removing 6 external deps is great for the dependency tree.

pablodeymo and others added 5 commits March 25, 2026 19:25
Conflicts were in field access patterns (.message → .block rename from main)
combined with hash method changes (tree_hash_root → H256(hash_tree_root()) from
libssz migration). Resolution: use main's .block.block field path with ssz-lib's
H256(hash_tree_root()) method.
@MegaRedHand MegaRedHand marked this pull request as ready for review March 31, 2026 15:56
@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. [primitives.rs](/home/runner/work/ethlambda/ethlambda/crates/common/types/src/primitives.rs#L50) changes H256 JSON serialization to bare hex ("abcd...") even though the type’s own docs say "0x..." at line 32 and Display still emits 0x... at line 117. That makes the wire format asymmetric with deserialization and changes every JSON/RPC response carrying roots. If any existing client or fixture expects standard 0x-prefixed hashes, this is a breaking interop regression.

  2. [primitives.rs](/home/runner/work/ethlambda/ethlambda/crates/common/types/src/primitives.rs#L86) introduces H256::from_slice as a truncating/zero-padding constructor, and [store.rs](/home/runner/work/ethlambda/ethlambda/crates/storage/src/store.rs#L205) uses it when decoding live-chain keys. A malformed or corrupted key now decodes to a different root instead of failing fast, which can poison fork-choice/live-chain traversal in hard-to-diagnose ways. This should be fallible and require exactly 32 bytes.

No other blocking correctness issues stood out in the SSZ migration itself.

I couldn’t run the Rust test suite in this sandbox: cargo needs to fetch/create dependencies under the default Cargo/Rustup locations, and that is blocked here. Residual risk: the only real-world gossipsub SSZ fixture test is currently ignored at [encoding.rs](/home/runner/work/ethlambda/ethlambda/crates/net/p2p/src/gossipsub/encoding.rs#L58), so wire-compatibility with existing peers is not being exercised automatically.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR migrates the entire SSZ dependency stack from ethereum_ssz / ssz_types / tree_hash to the libssz family of crates (libssz, libssz-derive, libssz-merkle, libssz-types). It also replaces ethereum-types::H256 with a hand-rolled newtype, and changes SignatureSize from a typenum expression to a plain const usize.

Key changes:

  • All SSZ trait names updated: Encode→SszEncode, Decode→SszDecode, TreeHash→HashTreeRoot, tree_hash_root()→hash_tree_root().
  • Collection types migrated to const-generic equivalents: VariableList<T,N>→SszList<T,N>, FixedVector<T,N>→SszVector<T,N>, BitList<N>→SszBitlist<N>.
  • H256 is now a local newtype struct H256(pub [u8; 32]) with manual Serialize/Deserialize, SSZ, and HashTreeRoot impls.
  • ValidatorPubkeyBytes promoted from a bare [u8; 52] alias to a newtype with manual SSZ/HashTreeRoot impls, because libssz does not provide implementations for the non-standard 52-byte size.
  • ethereum_ssz is retained as a direct dependency in ethlambda-crypto to bridge the Devnet2XmssAggregateSignature type from lean-multisig.
  • The pinned hash-root test in genesis.rs provides direct evidence that libssz_merkle hash tree roots are compatible with the old tree_hash values.

One issue to address: H256::Serialize produces hex without the 0x prefix (format!(\"{:x}\", self)) while Display and the doc both promise \"0x...\" form.

Confidence Score: 4/5

Safe to merge after fixing the H256 JSON serialization prefix issue; everything else is a mechanical API migration with well-bounded scope.

The migration is thorough and mechanically correct across 35 files. The pinned hash-root test in genesis.rs confirms encoding compatibility. The only P1 finding is H256::Serialize producing un-prefixed hex instead of the documented '0x...' form. All other findings are P2.

crates/common/types/src/primitives.rs — H256 Serialize implementation and from_slice behavior.

Important Files Changed

Filename Overview
crates/common/types/src/primitives.rs Defines the new H256 newtype and re-exports libssz types. H256 serialize is missing the 0x prefix (P1) and from_slice silently truncates/pads (P2).
crates/common/types/src/state.rs Replaces typenum-based limits with const generics, adds ValidatorPubkeyBytes newtype with manual SSZ/HashTreeRoot impls. State no longer derives Serialize (intentional).
crates/blockchain/state_transition/src/justified_slots_ops.rs Replaces BitList::union with manual bit-by-bit copy in extend_to_slot; semantically equivalent.
crates/blockchain/state_transition/src/lib.rs Updates tree hash calls, adapts attestation vote iteration to libssz API, functionally equivalent.
crates/blockchain/src/store.rs Mechanical SSZ API migration. num_participants now uses entries.len() which is equivalent to the old num_set_bits().
crates/storage/src/store.rs Straightforward as_ssz_bytes→to_ssz and Encode/Decode renames throughout. No logic changes.
crates/common/crypto/src/lib.rs Correctly keeps ethereum_ssz as a direct dep for lean-multisig compatibility.
Cargo.toml Replaces ethereum_ssz/ssz_types/tree_hash with libssz family. Also drops ethereum-types since H256 is now a custom type.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Before["Before"]
        A1["ethereum_ssz + ethereum_ssz_derive\nEncode / Decode"]
        B1["ssz_types\nVariableList / FixedVector / BitList"]
        C1["tree_hash + tree_hash_derive\nTreeHash / tree_hash_root()"]
        D1["ethereum-types\nH256 = Hash256"]
    end
    subgraph After["After"]
        A2["libssz + libssz-derive\nSszEncode / SszDecode"]
        B2["libssz-types\nSszList / SszVector / SszBitlist"]
        C2["libssz-merkle\nHashTreeRoot / hash_tree_root()"]
        D2["Local newtype\nstruct H256([u8;32])"]
    end
    A1 -->|renamed| A2
    B1 -->|const generic| B2
    C1 -->|renamed| C2
    D1 -->|hand-rolled| D2
    A1 -.->|retained in ethlambda-crypto\nfor lean-multisig bridge| A1
Loading

Comments Outside Diff (1)

  1. crates/blockchain/src/key_manager.rs, line 470-471 (link)

    P2 Debug format in user-facing error string

    format!("{e:?}") uses the Debug representation (includes Rust type names, enum variant names, etc.), producing noisy output like SignatureConversionError(SomeInternalError { ... }). A Display format (format!("{e}")) is more appropriate for strings that end up in error messages shown to callers.

    The same pattern was applied in signature.rs — aligning them would also make the error style consistent.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/blockchain/src/key_manager.rs
    Line: 470-471
    
    Comment:
    **Debug format in user-facing error string**
    
    `format!("{e:?}")` uses the `Debug` representation (includes Rust type names, enum variant names, etc.), producing noisy output like `SignatureConversionError(SomeInternalError { ... })`. A Display format (`format!("{e}")`) is more appropriate for strings that end up in error messages shown to callers.
    
    
    
    The same pattern was applied in `signature.rs` — aligning them would also make the error style consistent.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/common/types/src/primitives.rs
Line: 51-54

Comment:
**`H256` serializes without `0x` prefix, contradicting the doc comment**

The `Serialize` impl uses `format!("{:x}", self)` (via `LowerHex`) which produces raw hex digits with **no** `0x` prefix, e.g. `"abc123..."`. But the struct-level doc says _"Serialized as a `"0x..."` hex string"_ and `Display` adds the prefix. The deserialized path handles both forms, but any JSON consumer (checkpoint-sync clients, external tooling) expecting the `"0x..."` convention will receive un-prefixed strings instead.

```suggestion
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&format!("{}", self))
    }
```

Switching to `Display` (`"{}"`) is the simplest fix and keeps the format consistent with the doc comment and with Ethereum conventions.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/common/types/src/primitives.rs
Line: 86-91

Comment:
**`from_slice` silently truncates or pads instead of erroring**

If `bytes.len() > 32`, the extra bytes are silently dropped; if `bytes.len() < 32`, the result is zero-padded. Both cases are lossy and unexpected — callers probably intend to pass exactly 32 bytes. A panic (or a `Result`) on a wrong-length input would make misuse immediately visible rather than propagating a silently wrong hash.

```suggestion
    pub fn from_slice(bytes: &[u8]) -> Self {
        let arr: [u8; 32] = bytes.try_into().expect("H256::from_slice requires exactly 32 bytes");
        Self(arr)
    }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/blockchain/src/key_manager.rs
Line: 470-471

Comment:
**Debug format in user-facing error string**

`format!("{e:?}")` uses the `Debug` representation (includes Rust type names, enum variant names, etc.), producing noisy output like `SignatureConversionError(SomeInternalError { ... })`. A Display format (`format!("{e}")`) is more appropriate for strings that end up in error messages shown to callers.

```suggestion
            .map_err(|e| KeyManagerError::SignatureConversionError(format!("{e}")))?;
```

The same pattern was applied in `signature.rs` — aligning them would also make the error style consistent.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "chore: update to version 0.2 of libssz" | Re-trigger Greptile

@pablodeymo
Copy link
Copy Markdown
Collaborator

Bug: H256 JSON serialization drops 0x prefix

The old H256 type (tree_hash::Hash256 = alloy_primitives::B256 = FixedBytes<32>) serialized with a 0x prefix. The new custom H256 does not.

Old output (verified from alloy-primitives 1.4.1 bits/serde.rs):

"0x000000000123456789abcdef"

New output (primitives.rs:50-53, uses format!("{:x}", self) with a LowerHex impl that emits raw hex):

"000000000123456789abcdef"

This affects every JSON response containing H256 fields, including /lean/v0/checkpoints/justified and /lean/v0/fork_choice.

Suggested fix in crates/common/types/src/primitives.rs:

impl serde::Serialize for H256 {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&format!("0x{:x}", self))
    }
}

Note: the Deserialize impl already handles both formats (via strip_prefix("0x")), so only the Serialize side needs the fix.

Copy link
Copy Markdown
Collaborator

@pablodeymo pablodeymo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please review the reported bug

Copy link
Copy Markdown
Collaborator

@pablodeymo pablodeymo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: second pass on final state

Previous findings (H256 serialization, from_slice truncation) are fixed — nice work on the tests too.

Two remaining observations:

1. as_bytes() drops SSZ sentinel in dedup key (crates/storage/src/store.rs)

let key_bytes = payload.proof.participants.as_bytes().to_vec();

The old as_ssz_bytes() included the sentinel bit that encodes the bitlist length. as_bytes() does not, so two bitlists of different lengths with identical underlying bytes would produce the same key. Unlikely to trigger in practice (same data_root group → same length), but payload.proof.participants.to_ssz() preserves the original semantics and is a one-line fix.

2. num_participants metric uses entries.len() instead of bitfield count (crates/blockchain/src/store.rs)

The old num_set_bits() counted participants from the attestation bitfield. The new entries.len() counts store entries — equivalent today but fragile if the mapping ever diverges. aggregated.proof.participants.count_ones() is the direct libssz equivalent.

Neither is blocking. Everything else — wire format, hash roots, type limits, serde, bitfield ops — looks correct. Solid migration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants